/*
 * SRMI (an easy-to-use zero-code remoting library for Java)
 * Copyright (C) 2007  Mohammad Nabil Hussein
 * mohammad (dot) nabil (dot) h (at) gmail (dot) com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package SRMI.RemotingServices.ClassGeneration;

import SRMI.RemotingServices.PassableByRemote;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import org.apache.log4j.Logger;

/**
 * This class provides common methods for checking of 
 * compatibility with SRMI class criteria.
 * @author meemo
 */
public class ClassGenerationUtils
{
    static Set<Class> verifiedClasses = new HashSet<Class>();
    static Set<Class> noClasses = new HashSet<Class>();
    
    /**
     *  used to return error string in case of no-throw mode
     */
    public static String error;
    
    /**
     * used for further investigation for why classes aren't safe. If true, will
     * recursivly check the all the references classes.
     */
    public static boolean recurse = false;
    private static Stack<String> stack = new Stack<String>();
    private static Set<Class> currentClasses = new HashSet<Class>();
    
    /**
     * should we throw an exception or just return false ?
     */
    public static Boolean nothrow = false;
    
    private static final Logger l = Logger.getLogger(ClassGenerationUtils.class);
    
    /**
     * Checks whether the specified class meets the SRMI remoting criteria.
     * The return val and behaviour are determined by the static variables:
     * notrhow, and recurse. 
     * @param wrappedClass The class to check if it meets the criteria.
     * @return If nothrow is true, it returns false if the class doesn't meet
     * the criteria. The exact reason is left in the static variable error. If
     * nothrow is false, an exception is thrown in case it doesn't meet the 
     * criteria. In either case true is returned if it does meet it.
     */
    public static synchronized boolean AssertMeetsSrmiCriteria(Class wrappedClass)
    {
        l.info("Recursive analysis is turned " + (recurse?"on":"off"));
        
        if(verifiedClasses.contains(wrappedClass))
            return true;
        else if (noClasses.contains(wrappedClass))
            return false;
        else if (recurse &&  currentClasses.contains(wrappedClass))
            throw new UnsupportedOperationException("");
        
        if(wrappedClass.isInterface())
        {
            l.warn("Ignoring safety check for interface " + wrappedClass);
            return true;
        }
        
        currentClasses.add(wrappedClass);
        /*
         *physical:
         * must have default constructor ( for the dummy object on the client ) ( might be used to know u r dummy so u report error on any method call)
         *logical:
         * no final instance methods ( won't be overridden by the client's stub )
         * all data are PRIVATE ( so no one can access dummy object data )
         * ALL parameters and return types must be ( either passable-by-remote ) or serializable
         * I DO NOT SUPPORT GENERICS ... !!!
         */
        error = "";
        
        l.info("Testing " + wrappedClass.getName() + " for compliance :");
        
        // must not use &&, but use & cuz we want exhaustive criteria list ( avoiding short circuit effect )
        boolean ok = HasNonPrivateDefaultConstructor(wrappedClass);
        ok = ok & NoNonPrivateFinalInstanceMethods(wrappedClass);
        ok = ok & AllFieldsArePrivate(wrappedClass);
        ok = ok & AllParamsAndReturnTypesAreSerializable(wrappedClass);
        if(ok)
        {
            l.info("OK");
            verifiedClasses.add(wrappedClass);
        }
        else
        {
            l.info("NOT OK");
            noClasses.add(wrappedClass);
            if(recurse) currentClasses.remove(wrappedClass);
            
            error = "The class to wrap (" +
                    wrappedClass.toString() +
                    ") does not meet safe RemotingServices criteria :\n\t" + error;
            
            if(!nothrow)
            {
                throw new UnsupportedOperationException(error);
            }
            else
            {
                return false;
            }
        }
        if(recurse) currentClasses.remove(wrappedClass);
        return ok;
    }
    
    private static boolean HasNonPrivateDefaultConstructor(Class wrappedClass)
    {
        String msg = "has default constructor ... ";
        
        for(Constructor c : wrappedClass.getDeclaredConstructors())
        {
            if(c.getParameterTypes().length == 0
                    && !Modifier.isPrivate(c.getModifiers()))
            {
                l.info(msg + "yes");
                return true;
            }
        }
        
        l.info(msg + "no");
        error += "has no non-private default constructor\n\t";
        return false;
    }
    
    private static boolean NoNonPrivateFinalInstanceMethods(Class wrappedClass)
    {
        String msg = "no final instance methods ... ";
        boolean ok = true;
        for(Method m : wrappedClass.getDeclaredMethods())
        {
            if(Modifier.isFinal(m.getModifiers())
            && !Modifier.isStatic(m.getModifiers())
            && !Modifier.isPrivate(m.getModifiers())
            && !MemberOfObject(m))
            {
                l.info(msg + "no = " + m.toString());
                error +=  "Final non-private instance method " + m.toString() + "\n\t";
                ok = false;
            }
            // the only final instance methods in Object, is the locking facility and the getClass facility,
            // i don't think that should be a problem
        }
        l.info(msg + (ok?"yes":"no"));
        return ok;
    }
    
    private static boolean AllFieldsArePrivate(Class wrappedClass)
    {
        String msg = "all fields are private ... ";
        boolean ok = true;
        
        // we must traverse our way up in the inheritence tree
        // cuz getDeclaredFields doesn't return inherited fields
        while(wrappedClass != null)
        {
            for(Field f : wrappedClass.getDeclaredFields())
            {
                if(!Modifier.isPrivate(f.getModifiers())
                /*&& !Modifier.isStatic(f.getModifiers()) - even static fields must not be local - might be JVM dependant*/
                && !Modifier.isStatic(f.getModifiers()) // - but I must allow it -temporarily- in order for aspectj'd component to be remoted
                && !Modifier.isFinal(f.getModifiers())
                )
                {
                    l.info(msg + "no = " + f.toString());
                    error += "Non-private field " + f.toString() + "\n\t";
                    ok = false;
                }
            }
            wrappedClass = wrappedClass.getSuperclass();
        }
        l.info(msg + (ok?"yes":"no"));
        return ok;
    }
    
    private static boolean AllParamsAndReturnTypesAreSerializable(Class wrappedClass)
    {
        String msg = "all params and return types are serializable ... ";
        boolean ok = true;
        
        for(Method m : wrappedClass.getMethods())
        {
            if(MemberOfObject(m))
                continue;
            if(!IsSerializable(m.getReturnType()))
            {
                l.info(msg + "no = return type " + m.getReturnType().toString());
                error += "Non serializable " + m.getReturnType().toString() + " return type of " + m.toString() + "\n\t";
                ok = false;
            }
            for(Class type : m.getParameterTypes())
            {
                if(!IsSerializable(type))
                {
                    l.info(msg + "no = " + m.toString() + " : " + type.getName());
                    error += "Non serializable " + type.toString() + " parameter of " + m.toString() + "\n\t";
                    ok = false;
                }
            }
        }
        l.info(msg + (ok?"yes":"no"));
        return ok;
    }
    
    public static boolean MemberOfObject(Method m)
    {
        for(Method objm : Object.class.getMethods())
            if(objm.getName().equals(m.getName()) && SameArgs(objm,m))
                return true;
        return false;
    }
    
    private static boolean SameArgs(Method objm, Method m)
    {
        if(objm.getParameterTypes().length != m.getParameterTypes().length)
            return false;
        for(int methNo = 0; methNo < objm.getParameterTypes().length; methNo++)
            if(!objm.getParameterTypes()[methNo].equals(m.getParameterTypes()[methNo]))
                return false;
        return true;
        
    }
    
    private static boolean IsSerializable(Class<?> type)
    {
        Class<Serializable> ser = Serializable.class;
        boolean retval =
                type.isPrimitive() // includes void
                || ser.isAssignableFrom(type)
                || type.getAnnotation(PassableByRemote.class) != null
                || type.equals(String.class)
                ;
        
        if(recurse && !retval)
        {
            String ret_error = "";
            if(error.length() > 0)
                stack.push(error.charAt(0) + error.substring(1));
            else
                stack.push("");
            try
            {
                AssertMeetsSrmiCriteria(type);
            }
            catch(Exception ex)
            {
                if(ex.getMessage() != null && ex.getMessage().trim().length() > 0)
                {
                    System.out.println("=======>{");
                    System.out.println(ex.getMessage());
                    System.out.println("}<=======");
                }
            }
            error = stack.pop();
            error += ret_error;
        }
        return retval;
    }
}
